# Lambdas with object instances

require 'rubygems'
require 'serverside'

class Proc
  def proc_tag
    'proc_' + object_id.to_s(36).sub('-', '_')
  end
end

class Object
  def const_tag
    'C' + object_id.to_s(36).upcase.sub('-', '_')
  end
end

class RoutingConnection < ServerSide::Connection::Base
  # Converts a proc into a method, returning the method's name (as a symbol)
  def self.define_proc(&block)
    tag = block.proc_tag
    define_method(tag.to_sym, &block) unless instance_methods.include?(tag)
    tag.to_sym
  end
  
  # Converts a value into a local constant and freezes it. Returns the 
  # constant's tag name
  def self.cache_constant(value)
    tag = value.const_tag
    class_eval "#{tag} = #{value.inspect}.freeze" rescue nil
    tag
  end
  
  # Sets the default handler for incoming requests.
  def self.route_default(&block)
    define_method(:default_handler, &block)
  end
  
  # Adds a routing rule. The normalized rule is a hash containing keys (acting
  # as instance variable names) with patterns as values. If the rule is not a
  # hash, it is normalized into a pattern checked against the request path.
  # Pattern values can also be arrays, in any array member is checked as a pattern.
  def self.route(rule, &block)
    @@rules ||= []
    rule = {:path => rule} unless Hash === rule
    @@rules.unshift [rule, block]
    compile_rules
  end
  
  # Pattern for finding parameters inside patterns. Parameters are parts of the
  # pattern, which the routing pre-processor turns into sub-regexp that are
  # used to extract parameter values from the pattern.
  #
  # For example, matching '/controller/show' against '/controller/:action' will
  # give us @parameters[:action] #=> "show"
  ParamRegexp = /(?::([a-z]+))/
  
  # Returns the condition part for the key and value specified. The key is the
  # name of an instance variable and the value is a pattern to match against.
  # If the pattern contains parameters (for example, /controller/:action,) the
  # method creates a lambda for extracting the parameter values.
  def self.condition_part(key, value)
    p_parse, p_count = '', 0
    while (String === value) && (value =~ ParamRegexp)
      p_name = $1
      p_count += 1
      value.sub!(ParamRegexp, '(.*)')
      p_parse << "@parameters[:#{p_name}] = $#{p_count}\n"
    end
    cond = "(@#{key} =~ #{cache_constant(Regexp.new(value))})"
    if p_count == 0
      cond
    else
      tag = define_proc(&eval(
        "lambda {if #{cond}\n#{p_parse}true\nelse\nfalse\nend}"))
      "(#{tag})"
    end
  end
  
  # Converts a rule into an if statement. All keys in the rule are matched
  # against their respective values.
  def self.rule_to_statement(rule, block)
    proc_tag = define_proc(&block)
    if Proc === rule
      cond = define_proc(&rule).to_s
    else
      cond = rule.to_a.map {|kv|
        if Array === kv[1]
          '(' + kv[1].map {|v| condition_part(kv[0], v)}.join('||') + ')'
        else
          condition_part(kv[0], kv[1])
        end
      }.join('&&')
    end
    "return #{proc_tag} if #{cond}\n"
  end
  
  # Compiles all rules into a respond method that is invoked when a request
  # is received.
  def self.compile_rules
    code = @@rules.inject('lambda {') {|m, r| m << rule_to_statement(r[0], r[1])}
    code << 'default_handler}'
    define_method(:respond, &eval(code))
  end

  def unhandled
    send_response(403, 'text', 'No handler found.')
  end
  
  alias_method :default_handler, :unhandled
end

module Response
  Text = 'text'.freeze
  Hello = 'Hello there!'.freeze
  Brilliant = 'Brilliant!'.freeze
end

RoutingConnection.route_default do
  send_response(200, Response::Text, 'Hey, I don\'t know what to do with this request.')
end

RoutingConnection.route(:path => '/hello$') do
  send_response(200, Response::Text, Response::Hello)
end

RoutingConnection.route(:path => '/paula') do
  send_response(200, Response::Text, Response::Brilliant)
end

RoutingConnection.route(['/controller/:action/:id', '/controller/:action']) do
  send_response(200, Response::Text, "Action: #{@parameters[:action]}\nId: #{@parameters[:id]}")
end

ServerSide::Server.new('0.0.0.0', 8000, RoutingConnection)
